package bibliotheque.wave;

import java.util.ArrayList;
import java.util.Hashtable;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.concurrent.Semaphore;

/**
 * La classe WaveAgent permet de propager des vagues sur un graphe quelconque selon l'algorithme que nous avons étudié en cours, que nous avons adapté pour utiliser la technologie RMI comme support pour les communications.
 */
public abstract class WaveAgent extends UnicastRemoteObject implements WaveAgentRemote {
    
    /**
     * Thread qui gère l'initiation d'une vague.
     */
    private class ThreadInitiate extends Thread {
        private WaveAction wave_action;
        
        /**
         * Le Constructeur de la classe ThreadInitiate.
         * @param wave_action Le WaveAction qui doit etre propage avec la vague.
         */
        public ThreadInitiate(WaveAction wave_action) {
            this.wave_action = wave_action;
        }
        
        public void run() {
            Wave wave = null;
            ArrayList<WaveAgentRemote> wave_waits = null;
            
            WaveAgent.this.mutex.acquireUninterruptibly();
            this.wave_action.wave_id = new WaveId(WaveAgent.this.ip, WaveAgent.this.hl++);
            wave_waits = (ArrayList<WaveAgentRemote>)WaveAgent.this.neighbours.clone();
            wave = new Wave(this.wave_action.wave_id, null, wave_waits);
            WaveAgent.this.waves.put(this.wave_action.wave_id, wave);
            WaveAgent.this.mutex.release();
            
            try {
                wave.mutex.acquireUninterruptibly();
                this.wave_action.PropagateBefore(WaveAgent.this);
                for (int i = 0;i<wave_waits.size();i++) {
                    wave_waits.get(i).Propagate(WaveAgent.this, this.wave_action);
                }
                this.wave_action.PropagateAfter(WaveAgent.this);
                wave.first_mutex.release();
            } catch (Exception ex) {
                WaveError(WaveAgent.this.GetIp() + " : Erreur dans ThreadInitiate : " + ex.getMessage());
            } finally {
                wave.mutex.release();
            }
        }
    }
    
    /**
     * Thread qui gère la propagation d'une vague.
     */
    private class ThreadPropagate extends Thread {
        private WaveAgentRemote wave_agent;
        private WaveAction wave_action;
        
        /**
         * Le Constructeur de la classe ThreadPropagate.
         * @param wave_agent Le stub de WaveAgent qui a initie l'apel distant.
         * @param wave_action Le WaveAction qui doit etre propage avec la vague.
         */
        public ThreadPropagate(WaveAgentRemote wave_agent, WaveAction wave_action) {
            this.wave_agent = wave_agent;
            this.wave_action = wave_action;
        }
        
        public void run() {
            Wave wave = null;
            WaveAgent.PropagateState state = null;
            ArrayList<WaveAgentRemote> wave_waits = null;
            
            WaveAgent.this.mutex.acquireUninterruptibly();
            
            if(WaveAgent.this.neighbours.size() == 1) {
                //Reception d'une vague sur une feuille
                state = WaveAgent.PropagateState.LEEF;
            } else if(WaveAgent.this.waves.containsKey(wave_action.wave_id)) {
                //Reception d'une vague pas pour la premiere fois
                wave = WaveAgent.this.waves.get(wave_action.wave_id);
                state = WaveAgent.PropagateState.SECOND;
            } else {
                //Reception d'une vague pour la premiere fois
                wave_waits = (ArrayList<WaveAgentRemote>)WaveAgent.this.neighbours.clone();
                wave_waits.remove(this.wave_agent);
                wave = new Wave(this.wave_action.wave_id, this.wave_agent, wave_waits);
                WaveAgent.this.waves.put(this.wave_action.wave_id, wave);
                state = WaveAgent.PropagateState.FIRST;
            }
            WaveAgent.this.mutex.release();
            
            //blocage si la vague est recu pas pour la premiere fois, tant que premiere fois est pas fini de traiter
            if(state == WaveAgent.PropagateState.SECOND) {
                wave.first_mutex.acquireUninterruptibly();
                //deblocage en cascade
                wave.first_mutex.release();
            }
            
            try {
                //blocage du mutex d'exclusion mutuelle.
                //pas besoin de gerer l'exclusion mutuel sur une feuille.
                //On a d'ailleur meme pas cree l'objet wave dans ce cas!!!
                if(state != WaveAgent.PropagateState.LEEF) wave.mutex.acquireUninterruptibly();
                
                if(state == WaveAgent.PropagateState.LEEF) {
                    this.wave_action.PropagateBefore(WaveAgent.this);
                    this.wave_action.PropagateAfter(WaveAgent.this);
                    this.wave_action.Acknowledge(WaveAgent.this);
                    this.wave_action.LocalEnd(WaveAgent.this);
                    wave_agent.Acknowledge(WaveAgent.this, this.wave_action);
                } else if(state == WaveAgent.PropagateState.SECOND) {
                    //si la vague est recu pas pour la premiere fois, elle est traiter par comme un acquitement;
                    wave.Acknowledged(this.wave_agent);
                    WaveAgent.this.mutex.acquireUninterruptibly();
                    if(wave.GetState() != Wave.State.RUNNING) WaveAgent.this.waves.remove(this.wave_action.wave_id);
                    WaveAgent.this.mutex.release();
                    
                    if(wave.GetState() == Wave.State.GLOBALY_ENDED) {
                        this.wave_action.Acknowledge(WaveAgent.this);
                        this.wave_action.LocalEnd(WaveAgent.this);
                        this.wave_action.GlobalEnd(WaveAgent.this);
                    } else if(wave.GetState() == Wave.State.LOCALY_ENDED) {
                        this.wave_action.Acknowledge(WaveAgent.this);
                        this.wave_action.LocalEnd(WaveAgent.this);
                        wave.GetUp().Acknowledge(WaveAgent.this, this.wave_action);
                    } else {
                        this.wave_action.Acknowledge(WaveAgent.this);
                    }
                } else {
                    //si la vague est recu pour la premiere fois
                    this.wave_action.PropagateBefore(WaveAgent.this);
                    for (int i = 0;i<wave_waits.size();i++) {
                        wave_waits.get(i).Propagate(WaveAgent.this, this.wave_action);
                    }
                    this.wave_action.PropagateAfter(WaveAgent.this);
                    //libere le traitement des acquitement;
                    wave.first_mutex.release();
                }
            } catch (Exception ex) {
                WaveError(WaveAgent.this.GetIp() + " : Erreur dans ThreadPropagate : " + ex.getMessage());
            } finally {
                if(state != WaveAgent.PropagateState.LEEF) wave.mutex.release();
            }
        }
    }
    
    /**
     * Thread qui gère l'acquitement d'une vague.
     */
    private class ThreadAcknowledge extends Thread {
        private WaveAgentRemote wave_agent;
        private WaveAction wave_action;
        
        /**
         * Le Constructeur de la classe ThreadAcknowledge.
         * @param wave_agent Le stub de WaveAgent qui a initie l'apel distant.
         * @param wave_action Le WaveAction qui doit etre propage avec la vague.
         */
        public ThreadAcknowledge(WaveAgentRemote wave_agent, WaveAction wave_action) {
            this.wave_agent = wave_agent;
            this.wave_action = wave_action;
        }
        
        public void run() {
            Wave wave;
            
            WaveAgent.this.mutex.acquireUninterruptibly();
            wave = WaveAgent.this.waves.get(this.wave_action.wave_id);
            WaveAgent.this.mutex.release();
            
            wave.first_mutex.acquireUninterruptibly();
            wave.first_mutex.release();
            
            try {
                wave.mutex.acquireUninterruptibly();
                wave.Acknowledged(this.wave_agent);
                WaveAgent.this.mutex.acquireUninterruptibly();
                if(wave.GetState() != Wave.State.RUNNING) WaveAgent.this.waves.remove(this.wave_action.wave_id);
                WaveAgent.this.mutex.release();
                
                if(wave.GetState() == Wave.State.GLOBALY_ENDED) {
                    this.wave_action.Acknowledge(WaveAgent.this);
                    this.wave_action.LocalEnd(WaveAgent.this);
                    this.wave_action.GlobalEnd(WaveAgent.this);
                } else if(wave.GetState() == Wave.State.LOCALY_ENDED) {
                    this.wave_action.Acknowledge(WaveAgent.this);
                    this.wave_action.LocalEnd(WaveAgent.this);
                    wave.GetUp().Acknowledge(WaveAgent.this, this.wave_action);
                } else {
                    this.wave_action.Acknowledge(WaveAgent.this);
                }
            } catch (Exception ex) {
                WaveError(WaveAgent.this.ip + " : Erreur dans ThreadAcknowledge : " + ex.getMessage());
            } finally {
                wave.mutex.release();
            }
        }
    }
    
    /**
     * Les trois facons de traiter une vague lors de sa reception.
     */
    private enum PropagateState {LEEF, FIRST, SECOND};
    
    /**
     * L'identificateur unique du WaveAgent.
     */
    private String ip;
    /**
     * L'horloge locale du WaveAgent. Elle sert a nueroter les vagues.
     */
    private int hl;
    /**
     * Mutex pour proteger les variables du WaveAgent, et premiere file d'attente pour les vagues.
     */
    private Semaphore mutex;
    /**
     * La liste des vagues en cours de traitement.
     */
    private Hashtable<WaveId, Wave> waves;
    /**
     * La liste des voisins du WaveAgent.
     */
    protected ArrayList<WaveAgentRemote> neighbours;
    /**
     * Cette methode abstraite est invoque si un traitement du WaveAction echoue ou si une erreur de communication arrive.
     * @param ex Le message d'erreur a propager.
     */
    protected abstract void WaveError(String ex);
    
    /**
     * Le constructeur de WaveAgent.
     * @param ip L'identificateur unique du WaveAgent.
     * @throws java.rmi.RemoteException Necessaire car WaveAgent est un objet distant.
     */
    public WaveAgent(String ip) throws RemoteException {
        this.ip = ip;
        this.hl = 0;
        this.neighbours = new ArrayList<WaveAgentRemote>();
        this.waves = new Hashtable<WaveId, Wave>();
        this.mutex= new Semaphore(1);
    }
    /**
     * Retourne l'identificateur unique du WaveAgent;
     * @return L'identificateur unique du WaveAgent;
     */
    public String GetIp() {
        return(this.ip);
    }
    /**
     * Initie une vague.
     * @param wave_action Le WaveAction qui doit etre propage avec la vague.
     */
    public void Initiate(WaveAction wave_action) {
        new ThreadInitiate(wave_action).start();
    }
    
    /**
     * Propage une vague.
     * @param wave_agent Le stub de WaveAgent qui a initie l'apel distant.
     * @param wave_action Le WaveAction qui doit etre propage avec la vague.
     * @throws java.rmi.RemoteException Une erreur de liaison est arrivee.
     */
    public void Propagate(WaveAgentRemote wave_agent, WaveAction wave_action) throws RemoteException {
        new ThreadPropagate(wave_agent, wave_action).start();
    }
    
    /**
     * Acquite une vague.
     * @param wave_agent Le stub de WaveAgent qui a initie l'apel distant.
     * @param wave_action Le WaveAction qui doit etre propage avec la vague.
     * @throws java.rmi.RemoteException Une erreur de liaison est arrivee.
     */
    public void Acknowledge(WaveAgentRemote wave_agent, WaveAction wave_action) throws RemoteException  {
        new ThreadAcknowledge(wave_agent, wave_action).start();
    }
}
